Skip to content

feat(coverage): add coverage.changed option to report only changed files#9521

Open
kykim00 wants to merge 8 commits intovitest-dev:mainfrom
kykim00:feature/coverage-changed-option
Open

feat(coverage): add coverage.changed option to report only changed files#9521
kykim00 wants to merge 8 commits intovitest-dev:mainfrom
kykim00:feature/coverage-changed-option

Conversation

@kykim00
Copy link

@kykim00 kykim00 commented Jan 25, 2026

Description

This PR adds a new coverage.changed option that allows running all tests while only computing coverage for changed files. This is useful for CI pipelines that need to:

  1. Run the full test suite to ensure all tests pass
  2. Compute coverage only for files changed in a pull request

Previously, this required running two separate commands, which doubled the test execution time.

Resolves #8747

Usage

export default defineConfig({
  test: {
    coverage: {
      changed: 'HEAD', // or any git ref like 'main', 'origin/main'
    },
  },
})

The changed option accepts:

  • 'HEAD' - Compare against the last commit
  • 'main' - Compare against the main branch
  • 'origin/main' - Compare against remote main branch
  • Any valid git reference

Testing Instructions

  1. Navigate to the test directory:

    cd test/coverage-test
  2. Create a demo config file vitest.demo.config.ts:

    import { defineConfig } from 'vitest/config'
    
    export default defineConfig({
      test: {
        include: ['**/file-to-change.test.ts', '**/math.test.ts'],
        coverage: {
          enabled: true,
          provider: 'istanbul',
          reporter: ['text'],
          reportsDirectory: './coverage-demo',
        },
      },
    })
  3. Run tests without the changed option:

    pnpm vitest run --config=vitest.demo.config.ts

    Result: Coverage includes all files

    File               | % Stmts | % Branch | % Funcs | % Lines |
    -------------------|---------|----------|---------|---------|
    All files          |   33.33 |      100 |   33.33 |   33.33 |
     file-to-change.ts |      50 |      100 |      50 |      50 |
     math.ts           |      25 |      100 |      25 |      25 |
    
  4. Modify fixtures/src/file-to-change.ts (make any change)

  5. Update config to add changed: 'HEAD':

    coverage: {
      enabled: true,
      provider: 'istanbul',
      reporter: ['text'],
      reportsDirectory: './coverage-demo',
      changed: 'HEAD', // ← Add this line
    },
  6. Run tests again:

    pnpm vitest run --config=vitest.demo.config.ts

    Result: Coverage includes only changed files

    File               | % Stmts | % Branch | % Funcs | % Lines |
    -------------------|---------|----------|---------|---------|
    All files          |      50 |      100 |      50 |      50 |
     file-to-change.ts |      50 |      100 |      50 |      50 |
    

Note: Both test runs execute all tests (file-to-change.test.ts and math.test.ts), but the second run only reports coverage for the modified file.

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • Ideally, include a test that fails without this PR but passes with it.
  • Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.
  • Please check Allow edits by maintainers to make review process faster. Note that this option is not available for repositories that are owned by Github organizations.

Tests

  • Run the tests with pnpm test:ci.

Documentation

  • If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

@netlify
Copy link

netlify bot commented Jan 25, 2026

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit e845db8
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/6975a293447cbf00087afa42
😎 Deploy Preview https://deploy-preview-9521--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@AriPerkkio AriPerkkio self-requested a review January 27, 2026 06:00
Copy link
Member

@AriPerkkio AriPerkkio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial approach looks good, but I would rather see all this logic being added into BaseCoverageProvider so that it doesn't leak into core.ts for example.

In the beginning of reportCoverage:

async reportCoverage(coverageMap: unknown, { allTestsRun }: ReportContext): Promise<void> {

... we would do something like

if(this.options.changed) {
  this.changedFiles = this.getChangedFiles()
}

The getChangedFiles would use VitestGit like you've done now in core.ts. Then in isIncluded and getUntestedFilesByRoot we would be checking the this.changedFiles.

Would that work?

Comment on lines 1370 to 1387
private async getCoverageReportContext(allTestsRun: boolean): Promise<ReportContext> {
const reportContext: ReportContext = { allTestsRun }
const coverageChanged = this._coverageOptions.changed
if (!coverageChanged) {
return reportContext
}
const { VitestGit } = await import('./git')
const vitestGit = new VitestGit(this.config.root)
const changedFiles = await vitestGit.findChangedFiles({
changedSince: coverageChanged,
})
if (!changedFiles) {
process.exitCode = 1
throw new GitNotFoundError()
}
reportContext.changedFiles = Array.from(new Set(changedFiles))
return reportContext
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this logic should be here instead:

export class BaseCoverageProvider<Options extends ResolvedCoverageOptions<'istanbul' | 'v8'>> {

Comment on lines 354 to 356
if (this.changedFiles) {
finalCoverageMap.filter(filename => this.changedFiles!.has(slash(filename)))
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering at this point will be more expensive than filtering before remapping is done, e.g. in isIncluded.

@kykim00
Copy link
Author

kykim00 commented Feb 3, 2026

Sorry for the late response! That makes sense, I'll move the logic into BaseCoverageProvider and filter earlier in isIncluded. Will push the changes shortly.

@kykim00
Copy link
Author

kykim00 commented Feb 3, 2026

@AriPerkkio I updated the code to move the logic into BaseCoverageProvider and filtering now happens in isIncluded. Also fixed query-param/@fs normalization and added tests for remap + query-param cases. Please take another look when you get a chance!

@AriPerkkio AriPerkkio self-requested a review February 3, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run all tests, but report partial coverage, for CI purposes

2 participants